/*
 * Copyright (c) 1992, 1993 NeXT Computer, Inc.
 *
 * Adaptec AHA-1542 SCSI controller driver.
 *
 * HISTORY
 *
 * 13 Apr 1993	Doug Mitchell at NeXT
 *	Rewrote using I/O thread which has exclusive access to hardware.
 *
 * 22 Nov 1992 	Brian Pinkerton at NeXT
 *	Created from non-driverkit driver.
 */
 
#import <sys/types.h>
#import <bsd/sys/param.h>
#import <objc/Object.h>
#import <kernserv/queue.h>
#import <kernserv/prototypes.h>
#import <driverkit/return.h>
#import <driverkit/generalFuncs.h>
#import <driverkit/kernelDriver.h>
#import <driverkit/i386/kernelDriver.h>
#import <driverkit/interruptMsg.h>
#import <driverkit/scsiTypes.h>
#import <bsd/dev/scsireg.h>
#import	"scsivar.h"
#import <mach/message.h>
#import <mach/port.h>
#import <mach/mach_interface.h>
#import <machkit/NXLock.h>
#import <kernserv/ns_timer.h>
#import <driverkit/i386/ioPorts.h>

#import <driverkit/i386/directDevice.h>
#import <driverkit/i386/IOEISADeviceDescription.h>
#import <driverkit/IOSCSIController.h>
#import "AHAController.h"
#import "AHATypes.h"
#import "AHAInline.h"
#import "AHAThread.h"

extern unsigned ffs(unsigned mask);

/* 
 * Template for command message sent to the I/O thread.
 */
static msg_header_t AHAMessageTemplate = {
	0,					// msg_unused 
	1,					// msg_simple 
	sizeof(msg_header_t),			// msg_size 
	MSG_TYPE_NORMAL,			// msg_type 
	PORT_NULL,				// msg_local_port 
	PORT_NULL,				// msg_remote_port - TO
						// BE FILLED IN 
	IO_COMMAND_MSG				// msg_id 
};

/*
 * Private methods implemented in this file.
 */
@interface AHAController(PrivateMethods)
- (BOOL) probeAtPortBase 	: (IOEISAPortAddress) portBase;
- (IOReturn)executeCmdBuf	: (AHACommandBuf *)cmdBuf;
@end


@implementation AHAController

/*
 *  Probe, configure board, and init new instance.
 */
+ (BOOL)probe:deviceDescription
{
	AHAController	*aha = [self alloc];
	IORange		ioPort;

	ddm_init("AHAController probe\n", 1,2,3,4,5);
	aha->ioThreadRunning = NO;
	
	/*
	 *  Check that we have some IO Ports assigned, and probe using the
	 *  first IO Port.
	 *  -probeAtPortBase returns TRUE if there's an AHA Controller present.
	 */
	if ([deviceDescription numPortRanges] < 1) {
		IOLog("AHAController: can't determine port base!\n");
	    	[aha free];
		return NO;
	}
	ioPort = [deviceDescription portRangeList][0];
	if (![aha probeAtPortBase:ioPort.start]) {
		IOLog("Adaptec154x Not Found at port 0x%x\n", ioPort.start);
	    	[aha free];
		return NO;
	}
	return ([aha initFromDeviceDescription:deviceDescription] ? YES : NO);
}

- initFromDeviceDescription:deviceDescription
{
	unsigned Lun;
	kern_return_t krtn;
	
	ddm_init("AHAController initFromDeviceDescription\n", 1,2,3,4,5);
	
	queue_init(&outstandingQ);
	queue_init(&pendingQ);
	queue_init(&commandQ);
	commandLock      = [[NXLock alloc] init];
	outstandingCount = 0;
	dmaLockCount     = 0;
	numFreeCcbs      = AHA_QUEUE_SIZE;

	/*
	 * Note the I/O thread provided by IOSCSIController is running 
	 * upon return from the following method.
	 */
	if ([super initFromDeviceDescription:deviceDescription] == nil)
		return [self free];
	interruptPortKern = IOConvertPort([self interruptPort],
		IO_KernelIOTask,
		IO_Kernel);
	ioThreadRunning = YES;
	
	/*
	 *  Check the channel and irq we just found against what's in our
	 *  device description.  If they don't match, print a nasty warning
	 *  message and fail.
	 */
	if ([deviceDescription numChannels] < 1 ||
	    [deviceDescription channel] != config.dma_channel) {
		IOLog("AHAController: Actual DMA Channel (%d) doesn't match "
		      "configured value (%d)!\n", config.dma_channel,
		      ([deviceDescription numChannels] ? 
		      	[deviceDescription channel] : 0));
		return [self free];
	}

	if ([deviceDescription numInterrupts] < 1 ||
	    [deviceDescription interrupt] != config.irq) {
		IOLog("AHAController: Actual IRQ (%d) doesn't match "
		      "configured value (%d)!\n", config.irq,
		      ([deviceDescription numInterrupts] ?
		      	[deviceDescription interrupt] : 0));
		return [self free];
	}
	
	/*
	 *  Try to set up DMA.  Set the appropriate mode on the channel, and
	 *  try to enable it.
	 */
	if ([self setTransferMode:IO_Cascade forChannel:0] != IO_R_SUCCESS ||
	    [self enableChannel:0] != IO_R_SUCCESS) {
		IOLog("AHAController: couldn't init DMA!\n");
		return [super free];
	}

	/*
	 * Allocate Mailboxes and CCB's from low 16 M of memory.
	 */
	ahaMbArea = IOMallocLow(sizeof(struct aha_mb_area));
	ahaCcb = IOMallocLow(sizeof(struct ccb) * AHA_QUEUE_SIZE);

	/*
	 *  Initialize driver data structures: set up the mailbox in/out area,
	 *  and initialize the CCB queues.
	 *
	 *  Note that if we fail, the call to [super free] will release (and
	 *  disable) our resources (IRQ, DMA channel, portRanges).
	 */
	if (!aha_setup_mb_area(ioBase, ahaMbArea, ahaCcb)) {
		IOLog("AHAController: couldn't set up mailbox area!\n");
		return [self free];
	}

	[self resetStats];
	
	/*
	 * Reserve our target, enable interrupts, and go.
	 */
	for(Lun=0; Lun<SCSI_NLUNS; Lun++) {
		[self reserveTarget:config.scsi_id lun:Lun forOwner:self];
	}

	[self enableAllInterrupts];	/* turn on interrupts */
		
	/*
	 * Set the port queue length to the maximum size. 
	 */
	krtn = port_set_backlog(task_self(), [self interruptPort], 
		PORT_BACKLOG_MAX);
	if(krtn) {
		IOLog("%s: error %d on port_set_backlog()\n",
			[self name], krtn);
		/* Oh well... */
	}
	[self resetSCSIBus];
	[self registerDevice];		/* this is the last thing we do! */

	return self;
}

/*
 *  This is slightly incorrect, since we can actually handle more if some of
 *  the entries in the scatter/gather list can be more than PAGE_SIZE.  In
 *  practice, tho, they're never bigger, so we'll make this our max size.
 *  18 Mar 93 dmitch - use (AHA_SG_COUNT - 1) since requests (the first
 *  and the last) can cross page boundaries. 
 */
- (unsigned)maxTransfer
{
	return (AHA_SG_COUNT - 1) * PAGE_SIZE;
}

/* 
 * kill I/O thread, free up local dynamically allocated resources, 
 * then have super release resources. 
 */
- free
{
	AHACommandBuf cmdBuf;
	
	if(ioThreadRunning) {
		cmdBuf.op = AO_Abort;
		[self executeCmdBuf:&cmdBuf];
	}
	if(ahaMbArea) {
		IOFreeLow(ahaMbArea, sizeof(struct aha_mb_area));
	}
	if(ahaCcb) {
		IOFreeLow(ahaCcb, sizeof(struct ccb) * AHA_QUEUE_SIZE);
	}
	if(commandLock) {
		[commandLock free];
	}
	return [super free];
}

/*
 * Statistics support.
 */
- (unsigned int) numQueueSamples
{
	return totalCommands;
}


- (unsigned int) sumQueueLengths
{
	return queueLenTotal;
}


- (unsigned int) maxQueueLength
{
	return maxQueueLen;
}


- (void)resetStats
{
	totalCommands = 0;
	queueLenTotal = 0;
	maxQueueLen   = 0;
}

/*
 * Do a SCSI command, as specified by an IOSCSIRequest. All the 
 * work is done by the I/O thread.
 */
- (sc_status_t) executeRequest : (IOSCSIRequest *)scsiReq 
		    buffer : (void *)buffer 
		    client : (vm_task_t)client
{
	AHACommandBuf cmdBuf;
	
	ddm_exp("executeRequest: cmdBuf 0x%x\n", &cmdBuf, 2,3,4,5);
	
	cmdBuf.op      = AO_Execute;
	cmdBuf.scsiReq = scsiReq;
	cmdBuf.buffer  = buffer;
	cmdBuf.client  = client;
	
	[self executeCmdBuf:&cmdBuf];
	
	ddm_exp("executeRequest: cmdBuf 0x%x complete; result %d\n", 
		&cmdBuf, cmdBuf.result, 3,4,5);
	return cmdBuf.result;
}


/*
 *  Reset the SCSI bus. All the work is done by the I/O thread.
 */
- (sc_status_t)resetSCSIBus
{
	AHACommandBuf cmdBuf;
	
	ddm_exp("resetSCSIBus: cmdBuf 0x%x\n", &cmdBuf, 2,3,4,5);

	cmdBuf.op = AO_Reset;
	[self executeCmdBuf:&cmdBuf];
	return cmdBuf.result;
}
/*
 * The following 6 methods are all called from the I/O thread in 
 * IODirectDevice. 
 */
 
/*
 * Called from the I/O thread when it receives an interrupt message.
 */
- (void)interruptOccurred
{
	struct ccb	*ccb;
	aha_intr_reg_t	intr;
	aha_mb_t	*mb;
	int		i;

	ddm_thr("interruptOccurred\n", 1,2,3,4,5);

	intr = aha_get_intr(ioBase);
	aha_clr_intr(ioBase);

	if (!intr.mb_in_full)
		return;

	/*
	 * Find all ccb's which the controller has marked completed
	 * and commandComplete: them.
	 */
	mb = ahaMbArea->mb_in;
	for (i = 0; i < AHA_MB_CNT; i++, mb++) {
		if (mb->mb_stat != AHA_MB_IN_FREE) {
			
			/*
			 * FIXME - need IOVirtualFromPhysical(); assume for
			 * now that we can access all physical addresses.
			 */
			ccb = (struct ccb *)aha_get_24(mb->ccb_addr);
			mb->mb_stat = AHA_MB_IN_FREE;	
			queue_remove(&outstandingQ, ccb, struct ccb *, ccbQ);
			ASSERT(outstandingCount != 0);
			outstandingCount--;
			
			[self commandCompleted:ccb reason:CS_Complete];
		}
	}
	
	/*
	 * Handle possible pending commands (now that we've dequeued at least
	 * one CCB).
	 */
	[self runPendingCommands];
	
	/*
	 * One more thing - since we probably just freed up at least one
	 * ccb, process possible entries waiting in commandQ.
	 */
	[self commandRequestOccurred]; 
	ddm_thr("interruptOccurred: DONE\n", 1,2,3,4,5);
}

/*
 * These three should not occur; they are here as error traps. All three are 
 * called out from the I/O thread upon receipt of messages which it should
 * not be seeing.
 */
- (void)interruptOccurredAt:(int)localNum
{
	IOLog("%s: interruptOccurredAt:%d\n", [self name], localNum);
}

- (void)otherOccurred:(int)id
{
	IOLog("%s: otherOccurred:%d\n", [self name], id);
}

- (void)receiveMsg
{
	IOLog("%s: receiveMsg\n", [self name]);
	
	/*
	 * We have to let IODirectDevice take care of this (i.e., dequeue the
	 * bogus message).
	 */
	[super receiveMsg];
}

/*
 * Called from the I/O thread when it receives a timeout
 * message. We send these messages ourself from ahaTimeout() in 
 * AHAThread.m.
 */
- (void)timeoutOccurred
{
	struct ccb	*ccb, *nextCcb;
	ns_time_t	now;
	queue_head_t	*queue;
	BOOL		ccbTimedOut = NO;
	AHACommandBuf	*cmdBuf;
	IOSCSIRequest	*scsiReq;
	
	ddm_thr("timeoutOccurred\n", 1,2,3,4,5);
	
	IOGetTimestamp(&now);

	/*
	 *  Scan the list of outstanding and pending commands, and time
	 *  out any ones whose time is past.
	 */

	for (queue = &outstandingQ; queue != &pendingQ; queue = &pendingQ) {

	    ccb = (struct ccb *) queue_first(&outstandingQ);
	    while (!queue_end(&outstandingQ, (queue_entry_t) ccb)) {
	        ns_time_t	expire;
	        
		cmdBuf  = ccb->cmdBuf;
		scsiReq = cmdBuf->scsiReq;
		expire = ccb->startTime + 
		    1000000000ULL * 
		    	(unsigned long long)scsiReq->timeoutLength;
	        if (now >= expire) {
			/*
			 *  Remove ccb from the oustanding queue and
			 *  complete it.
			 */
			nextCcb = (struct ccb *) queue_next(&ccb->ccbQ);
			queue_remove(&outstandingQ, ccb, struct ccb *, ccbQ);
			if(queue == &outstandingQ) {
				ASSERT(outstandingCount != 0);
				outstandingCount--;
			}
			[self commandCompleted:ccb reason:CS_Timeout];
			ccb = nextCcb;
			ccbTimedOut = YES;
		}
		else {
			ccb = (struct ccb *) queue_next(&ccb->ccbQ);
		}
	    }
	}

	/*
	 * Reset bus. This also completes all I/Os in outstandingQ with
	 * status CS_Reset.
	 */
	if(ccbTimedOut) {
		[self threadResetBus:NULL];
	}
	ddm_thr("timeoutOccurred: DONE\n", 1,2,3,4,5);
}

/*
 * Process all commands in commandQ. If we run out of ccb's during this 
 * method, we abort, leaving commands enqueued; these will be handled after
 * subqueuent interrupts.
 *
 * This is called either as a result of an IO_COMMAND_MSG message being 
 * received by the I/O thread, or upon completion of interrupt handling. In
 * either case, it runs in the context of the I/O thread.
 */
- (void)commandRequestOccurred
{
	AHACommandBuf *cmdBuf;
	
	ddm_thr("commandRequestOccurred: top\n", 1,2,3,4,5);
	[commandLock lock];
	while(!queue_empty(&commandQ)) {
		cmdBuf = (AHACommandBuf *) queue_first(&commandQ);
		queue_remove(&commandQ, cmdBuf, AHACommandBuf *, link);
		[commandLock unlock];
		switch(cmdBuf->op) {
		    case AO_Reset:
		    	[self threadResetBus:cmdBuf];
			break;
			
		    case AO_Abort:
			/*
			 * First notify caller of completion, then 
			 * self-terminate.
			 */
			[cmdBuf->cmdLock lock];
			[cmdBuf->cmdLock unlockWith:CMD_COMPLETE];
			IOExitThread();
			/* not reached */
			
		    case AO_Execute:
		    	if([self threadExecuteRequest:cmdBuf]) {
				/*
				 * No more CCBs available. Abort this entire
				 * method. Enqueue this request on the head
				 * of commandQ for future processing.
				 */
				[commandLock lock];
				queue_enter_first(&commandQ, cmdBuf,
					AHACommandBuf *, link);
				[commandLock unlock];
				ddm_thr("processCommandQ: no more ccbs; "
					"cmdBuf 0x%x\n", cmdBuf, 2,3,4,5);
				goto out;
					
			}
		}
		[commandLock lock];
	}
	[commandLock unlock];
out:
	ddm_thr("commandRequestOccurred: DONE\n", 1,2,3,4,5);
	return;
}


@end	/* methods declared in AHAController.h */

@implementation AHAController(PrivateMethods)

- (BOOL) probeAtPortBase:(IOEISAPortAddress) portBase
{
	aha_inquiry_t	inquiry;
    
	ddm_init("AHAController probeAtPortBase\n", 1,2,3,4,5);

	ioBase = portBase;
	aha_reset_board(ioBase, ahaBoardId);

	/*
	 *  Do an inquiry to find out the board id and other things that
	 *  we won't check.
	 */
	if (!aha_probe_cmd(ioBase, AHA_CMD_DO_INQUIRY, 0, 0,
	    (unsigned char *)&inquiry, sizeof(inquiry), TRUE)) {
	    	ddm_init("  ..inquiry command failed\n", 1,2,3,4,5);
		return FALSE;
	}
	
	ahaBoardId = inquiry.board_id;

	switch (ahaBoardId) {

	case AHA_1540_16HEAD:
	case AHA_154xB:
	case AHA_1540_64HEAD:
	case AHA_1640:
	case AHA_174xA:
	case AHA_154xC:
		break;
	default:
		if (ahaBoardId < AHA_154xC) {
		    	ddm_init("..bogus board ID (0x%x)\n", ahaBoardId, 
				2,3,4,5);
			return FALSE;
		}
	}

	/*
	 *  Attempt to read the configuration data from the board.
	 *  If this succeeds, then we have successfully probed.
	 */
	if (!aha_probe_cmd(ioBase, AHA_CMD_GET_CONFIG, 0, 0,
	                   (unsigned char *)&config, sizeof(config), TRUE)) {
	    	ddm_init("  ..get config command failed\n", 1,2,3,4,5);

	    	return FALSE;
	}
	
	/*
	 *  Decode the values in the config struct.
	 */
	config.irq = ffs((unsigned int) config.irq) + 8;
	config.dma_channel = ffs((unsigned int) config.dma_channel) - 1;

	IOLog("Adaptec154x at port 0x%x irq %d\n", 
		portBase, config.irq);
	return TRUE;
}

/*
 * Pass one AHACommandBuf to the I/O thread; wait for completion. 
 * Normal completion status is in cmdBuf->status; a non-zero return 
 * from this function indicates a Mach IPC error.
 *
 * This method allocates and frees cmdBuf->cmdLock.
 */
- (IOReturn)executeCmdBuf : (AHACommandBuf *)cmdBuf
{
	msg_header_t msg = AHAMessageTemplate;
	kern_return_t krtn;
	IOReturn rtn = IO_R_SUCCESS;
	
	cmdBuf->cmdLock = [[NXConditionLock alloc] initWith:CMD_PENDING];
	[commandLock lock];
	queue_enter(&commandQ, cmdBuf, AHACommandBuf *, link);
	[commandLock unlock];
	
	/*
	 * Create a Mach message and send it in order to wake up the 
	 * I/O thread.
	 */
	msg.msg_remote_port = interruptPortKern;
	krtn = msg_send_from_kernel(&msg, MSG_OPTION_NONE, 0);
	if(krtn) {
		IOLog("%s: msg_send_from_kernel() returned %d\n", 
			[self name], krtn);
		rtn = IO_R_IPC_FAILURE;
		goto out;
	}
	
	/*
	 * Wait for I/O complete.
	 */
	ddm_exp("executeCmdBuf: waiting for completion on cmdBuf 0x%x\n",
		cmdBuf, 2,3,4,5);
	[cmdBuf->cmdLock lockWhen:CMD_COMPLETE];
	ddm_exp("executeCmdBuf: cmdBuf 0x%x complete\n",
		cmdBuf, 2,3,4,5);
out:
	[cmdBuf->cmdLock free];
	return rtn;
}

@end	/* AHAController(PrivateMethods) */



